﻿namespace MvvmTools.Tests
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reactive.Linq;
    using System.Reactive.Subjects;
    using NUnit.Framework;
    using Shouldly;

    /// <summary>
    /// Tests for NotifyPropertyChangedExtension
    /// </summary>
    [TestFixture]
    public class NotifyPropertyChangedExtensionFixture
    {
        public interface IMyClass : INotifyPropertyChanged
        {
            IMyClass2 Class2 { get; set; }

            void ChangeClass2();
        }

        public interface IMyClass2 : INotifyPropertyChanged
        {
            IMyClass3 Class3 { get; set; }
        }

        public interface IMyClass3 : INotifyPropertyChanged
        {
            int Value1 { get; set; }

            int Value2 { get; set; }

            IObservable<int> IntValues { get; set; }
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldSupportDistinctUntilChangedWhenMergeToSourcesFromOneParent()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            var s = target.OnChange(i => i.Class2).OnChange(i => i.Class3);
            var s1 = s.OnValueChange(i => i.Value1);
            var s2 = s.OnValueChange(i => i.Value2);

            using (s1.Merge(s2).DistinctUntilChanged().Subscribe(props => changed++))
            {
                target.Class2.Class3 = new MyClass3();
            }

            // Then
            changed.ShouldBe(1);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldSupportDistinctUntilChangedWhenUseCallerMemberName()
        {
            // Given
            var changed = 0;
            var target = new MyClassWithCallerMemberName();
            target.Class2 = new MyClass2WithCallerMemberName();
            target.Class2.Class3 = new MyClass3WithCallerMemberName();

            var s = target.OnChange(i => i.Class2).OnChange(i => i.Class3);
            var s1 = s.OnValueChange(i => i.Value1);
            var s2 = s.OnValueChange(i => i.Value2);

            using (s1.Merge(s2).DistinctUntilChanged().Subscribe(props => changed++))
            {
                target.Class2.Class3 = new MyClass3();
            }

            // Then
            changed.ShouldBe(1);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldProvideISubscriberForReactiveExtensionsWhenNotifyPropertyChangedReused()
        {
            // Given
            var changed1 = 0;
            var changed2 = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            var class2Subs = target.OnChange(objThis => objThis.Class2);
            var class3Subs = class2Subs.OnChange(p => p.Class3);
            var subs = class3Subs.OnValueChange(p => p.Value1);

            subs.Subscribe(props => changed1++);
            subs.Subscribe(props => changed2++);

            target.Class2.Class3 = new MyClass3();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // Then
            changed1.ShouldBe(3);
            changed2.ShouldBe(3);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldProvideISubscriberForReactiveExtensionsWithMultSources()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // When
            var events =
                target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1)
                .Merge(target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value2)).Distinct();

            events.Subscribe(props => changed++);

            target.Class2.Class3.Value1 = 10;
            target.Class2.Class3.Value2 = 20;

            // Then
            Assert.AreEqual(2, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldUnsubscribeForReactiveExtensions()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();
            var events =
                from ev in target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1)
                select ev;
            var subscription = events.Subscribe(props => changed++);

            // When
            subscription.Dispose();

            target.Class2.Class3 = new MyClass3();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // Then
            Assert.AreEqual(0, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldCallUpdateWhenPropertyChanged()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // When
            target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1).Subscribe(i => changed++);
            target.Class2.Class3 = new MyClass3();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // Then
            Assert.AreEqual(3, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldCallUpdateWhenAnyChangesAreOccured()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // When
            target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnAnyChanges().Subscribe(i => changed++);
            target.Class2.Class3.Value1 = target.Class2.Class3.Value1 + 1;
            target.Class2.Class3.Value2 = target.Class2.Class3.Value2 + 1;

            // Then
            Assert.AreEqual(2, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldCallUpdateWhenPropertyChangedForMutllipleChildProperties()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // When
            var observer = target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3);
            observer.Subscribe(i => changed++);
            observer.OnValueChange(p => p.Value1).Subscribe(i => changed++);
            observer.OnValueChange(p => p.Value2).Subscribe(i => changed++);

            target.Class2.Class3 = new MyClass3();

            // Then
            Assert.AreEqual(3, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldCallUpdateWhenPropertyChangedComplex()
        {
            // Given
            int changed = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // When
            target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1).Subscribe(i => changed++);
            target.Class2.Class3 = new MyClass3();
            Assert.AreEqual(1, changed);
            target.Class2 = new MyClass2();
            Assert.AreEqual(2, changed);
            target.Class2.Class3 = new MyClass3();
            Assert.AreEqual(3, changed);
            target.Class2.Class3.Value1 = 8;
            Assert.AreEqual(4, changed);
            var newClass2 = new MyClass2();
            newClass2.Class3 = new MyClass3();
            target.Class2 = newClass2;
            Assert.AreEqual(5, changed);
            target.Class2.Class3.Value1 = 9;

            // Then
            Assert.AreEqual(6, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldUnsuscribeFromAllPropertiesWhenLastLeafOfDependencyIsDisposing()
        {
            // Given
            int changed = 0;
            var target = new MyClass();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // When
            // ReSharper disable once AccessToModifiedClosure
            var subs = target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1).Subscribe(i => changed++);
            target.Class2.Class3 = new MyClass3();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            subs.Dispose();
            changed = 0;

            target.Class2.Class3 = new MyClass3();
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();

            // Then
            Assert.AreEqual(0, changed);
        }

        /// <summary>
        /// Check that extension throws exception
        /// </summary>
        [Test, ExpectedException(typeof(InvalidOperationException))]
        public void NotifyPropertyChangedExtensionShouldThrowInvalidOperationExWhenNotLastLeafOfDependencyIsDisposing()
        {
            // Given
            var target = new MyClass();

            // When
            var subs = target.OnChange(objThis => objThis.Class2);
            subs.Subscribe(i => { });
            subs.OnChange(p => p.Class3).OnValueChange(p => p.Value1).Subscribe(i => { });

            // Then
            subs.Dispose();
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldCallUpdateWhenPropertyChangedFromInternalFunction()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1).Subscribe(i => changed++);
            target.Class2 = new MyClass2();

            // When
            target.ChangeClass2();

            // Then
            Assert.AreEqual(2, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldSuspendedUpdatesNotificationWhenIPropertyObserverSuspendIsInvoked()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            var subs = target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1);
            // ReSharper disable once AccessToModifiedClosure
            subs.Subscribe(i => changed++);
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();
            changed = 0;

            // When
            using (subs.Suspend())
            {
                target.Class2.Class3.Value1 = 1;
            }

            // Then
            Assert.AreEqual(0, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldSuspendedUpdatesNotificationWhenChangeMidllePropertyIPropertyObserverSuspendIsInvoked()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            var subs = target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1);
            // ReSharper disable once AccessToModifiedClosure
            subs.Subscribe(i => changed++);
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();
            changed = 0;

            // When
            using (subs.Suspend())
            {
                target.Class2 = new MyClass2();
            }

            // Then
            Assert.AreEqual(0, changed);
        }

        [Test]
        public void NotifyPropertyChangedExtensionShouldSuspendedUpdatesNotificationWhenIPropertyObserverSuspendHandleIsDisposed()
        {
            // Given
            var changed = 0;
            var target = new MyClass();
            var subs = target.OnChange(objThis => objThis.Class2).OnChange(p => p.Class3).OnValueChange(p => p.Value1);
            // ReSharper disable once AccessToModifiedClosure
            subs.Subscribe(i => changed++);
            target.Class2 = new MyClass2();
            target.Class2.Class3 = new MyClass3();
            changed = 0;

            using (subs.Suspend())
            {
                // When
                target.Class2.Class3.Value1 = 1;
            }

            target.Class2.Class3.Value1 = 2;

            // Then
            Assert.AreEqual(1, changed);
        }
        
        [Test]
        public void ShouldSubscribeToObservable()
        {
            // Given
            var changed = 0;
            var target = new MyClass2 { Class3 = new MyClass3() };

            using (target.OnChange(o => o.Class3).OnObservableChange(o => o.IntValues).Subscribe(_ => changed++))
            {
                // When
                target.Class3.IntValues = Observable.Range(0, 9); // The first change.

                // Then
                Assert.AreEqual(10, changed);
            }
        }

        [Test]
        public void ShouldSubscribeToObservableAndIfChangedNotifyTheRequiredPropertyName()
        {
            // Given
            var target = new MyClass3();
            var changedProps = new List<PropertyChange>();
            var expectedPropertyName = ExpressionExtensions.GetParameterName((MyClass3 o) => o.IntValues);

            using (target.OnObservableChange(o => o.IntValues).Subscribe(changedProps.Add))
            {
                // When
                target.IntValues = Observable.Return(0);

                // Then
                changedProps.Select(i => i.PropertyName).Distinct().Single().ShouldBe(expectedPropertyName);
            }
        }

        [Test]
        public void ShouldSubsribeToObservable()
        {
            // Given
            var changed = 0;
            var target = new MyClass2 { Class3 = new MyClass3() };

            var subject = new Subject<int>();
            target.Class3.IntValues = subject;

            using (target.OnChange(o => o.Class3).OnObservableChange(o => o.IntValues).Subscribe(_ => changed++))
            {
                // When
                subject.OnNext(6);

                // Then
                changed.ShouldBe(1);
            }
        }

        [Test]
        public void ShouldSuspendSubscriptionToObservable()
        {
            // Given
            var changed = 0;
            var target = new MyClass2 { Class3 = new MyClass3() };

            var subject = new Subject<int>();
            target.Class3.IntValues = subject;

            using (var subscription = target.OnChange(o => o.Class3).OnObservableChange(o => o.IntValues))
            {
                // When
                subscription.Subscribe(i => changed++);
                using (subscription.Suspend())
                {
                    subject.OnNext(5);
                }

                // Then
                changed.ShouldBe(0);
            }
        }

        [Test]
        public void ShouldUnsubscribeCallByOneLeaf()
        {
            // Given
            var count = 0;
            var target = new MyClass { Class2 = new MyClass2 { Class3 = new MyClass3() } };
            var subsTarget = target.OnChange(t => t.Class2).OnChange(t => t.Class3);
            var subs1 = subsTarget.OnValueChange(p => p.Value1);
            var subs2 = subsTarget.OnValueChange(p => p.Value2);

            subsTarget.Subscribe(i => { count++; });
            subs1.Subscribe(i => { count++; });
            subs2.Subscribe(i => { count++; });

            // When
            subs2.Dispose();
            target.Class2.Class3.Value1 = 10;

            // Then
            count.ShouldBe(0);
        }

        [Test]
        public void ShouldNotUnsubscribeObsrvableByOneLeaf()
        {
            // Given
            int count1 = 0;
            int count2 = 0;
            var target = new MyClass { Class2 = new MyClass2 { Class3 = new MyClass3() } };
            var subsTarget = target.OnChange(t => t.Class2).OnChange(t => t.Class3);
            subsTarget.OnValueChange(p => p.Value1).Subscribe(i => { count1++; });
            var subs2 = subsTarget.OnValueChange(p => p.Value2).Subscribe(i => { count2++; });

            // When
            subs2.Dispose();
            target.Class2.Class3 = new MyClass3();

            // Then
            count1.ShouldBe(1);
            count2.ShouldBe(0);
        }

        [Test]
        public void ShouldSupportStringEmptyModificationForOnValueChange()
        {
            // Given
            var class3 = new MyClass3();
            var count1 = 0;
            class3.OnValueChange(p => p.Value1).Subscribe(i => { count1++; });

            // When
            class3.RaiseEmptyPropertyChanged();

            // Then
            count1.ShouldBe(1);
        }

        [Test]
        public void ShouldSupportStringEmptyModificationForOnChange()
        {
            // Given
            var class2 = new MyClass2();
            var count1 = 0;
            class2.OnChange(p => p.Class3).Subscribe(i => { count1++; });

            // When
            class2.RaiseEmptyPropertyChanged();

            // Then
            count1.ShouldBe(1);
        }

        [Test]
        public void ShouldMergeSeveralChanges()
        {
            // Given
            var class3 = new MyClass3();
            var count1 = 0;
            Observable.Merge(class3.OnValueChange(p => p.Value1), class3.OnValueChange(p => p.Value2))
                .DistinctUntilChanged()
                .Subscribe(i => { count1++; });

            // When
            class3.RaiseEmptyPropertyChanged();

            // Then
            count1.ShouldBe(1);
        }

        internal class MyClass : NotificationModel, IMyClass
        {
            private IMyClass2 _class2;

            public IMyClass2 Class2
            {
                get
                {
                    return _class2;
                }

                set
                {
                    UpdateProperty(() => Class2, ref _class2, value);
                }
            }

            public void ChangeClass2()
            {
                Class2 = new MyClass2();
            }
        }

        internal class MyClassWithCallerMemberName : NotificationModel, IMyClass
        {
            private IMyClass2 _class2;

            public IMyClass2 Class2
            {
                get
                {
                    return _class2;
                }

                set
                {
                    UpdateProperty(ref _class2, value);
                }
            }

            public void ChangeClass2()
            {
                Class2 = new MyClass2();
            }
        }

        internal class MyClass2 : NotificationModel, IMyClass2
        {
            private IMyClass3 _class3;

            public IMyClass3 Class3
            {
                get
                {
                    return _class3;
                }

                set
                {
                    UpdateProperty(() => Class3, ref _class3, value);
                }
            }

            public void RaiseEmptyPropertyChanged()
            {
                OnPropertyChanged(string.Empty);
            }
        }

        internal class MyClass2WithCallerMemberName : NotificationModel, IMyClass2
        {
            private IMyClass3 _class3;

            public IMyClass3 Class3
            {
                get
                {
                    return _class3;
                }

                set
                {
                    UpdateProperty(ref _class3, value);
                }
            }

            public void RaiseEmptyPropertyChanged()
            {
                OnPropertyChanged(string.Empty);
            }
        }

        internal class MyClass3 : NotificationModel, IMyClass3
        {
            private int _value1;
            private int _value2;
            private IObservable<int> _intValues;

            public int Value1
            {
                get
                {
                    return this._value1;
                }

                set
                {
                    UpdateProperty(() => Value1, ref _value1, value);
                }
            }

            public int Value2
            {
                get
                {
                    return _value2;
                }

                set
                {
                    UpdateProperty(() => Value2, ref _value2, value);
                }
            }

            public IObservable<int> IntValues
            {
                get
                {
                    return _intValues;
                }

                set
                {
                    UpdateProperty(() => IntValues, ref _intValues, value);
                }
            }

            public void RaiseEmptyPropertyChanged()
            {
                OnPropertyChanged(string.Empty);
            }
        }

        internal class MyClass3WithCallerMemberName : NotificationModel, IMyClass3
        {
            private int _value1;
            private int _value2;
            private IObservable<int> _intValues;

            public int Value1
            {
                get
                {
                    return this._value1;
                }

                set
                {
                    UpdateProperty(ref _value1, value);
                }
            }

            public int Value2
            {
                get
                {
                    return _value2;
                }

                set
                {
                    UpdateProperty(ref _value2, value);
                }
            }

            public IObservable<int> IntValues
            {
                get
                {
                    return _intValues;
                }

                set
                {
                    UpdateProperty(ref _intValues, value);
                }
            }

            public void RaiseEmptyPropertyChanged()
            {
                OnPropertyChanged(string.Empty);
            }
        }
    }
}